Newer
Older
client_dwatch / app / api / ds-devices / [serial_number] / logs / exports / route.ts
/**
 * POST /ds-devices/{serial_number}/logs/exports — ログのエクスポートの準備(指示書AY 段階7-1。D14・D-179)
 *
 * 仕様の正は `d-watch-api.yaml` の /ds-devices/{serial_number}/logs/exports。
 * 制御盤に zip を作らせ、prepared が届くまで最大50秒待ってから 201(受け取りは `download_url`)。
 * ★サーバーには保存しない★。★POST が切れたら(`request.signal`)エクスポートを取り消す(4-9)★。
 */
import { NextResponse } from "next/server";
import { parsePathSerialNumber } from "@/lib/api/path-params";
import { db } from "@/lib/db";
import { USE_FIXTURES } from "@/lib/db/use-fixtures";
import { errorBody, jsonError } from "@/lib/ds-agent/agent-request";
import { ensureFixtureAgent } from "@/lib/ds-agent/fixture-agent";
import { getRelay } from "@/lib/ds-agent/relay";
import { ensureDsAgentLogging } from "@/lib/ds-agent/relay-log";
import { parseExportRequest } from "@/lib/ds-logs/validate";
import { logEvent, withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { DsLogExportResponse } from "@/lib/types";

const ROUTE = "/api/ds-devices/[serial_number]/logs/exports";

export async function POST(
  request: Request,
  { params }: { params: Promise<{ serial_number: string }> }
) {
  return withRequestId(request, async () => {
    ensureDsAgentLogging();
    const { serial_number: serialRaw } = await params;
    const parsedSerial = parsePathSerialNumber(serialRaw);
    if (!parsedSerial.ok) return NextResponse.json(parsedSerial.error, { status: parsedSerial.status });
    const serial = parsedSerial.query;

    // ★JSON でない・オブジェクトでない本体は 422「JSONの形式が不正です」(note と同じ。V8・V9)★
    const parsedBody = parseExportRequest(await request.text());
    if (!parsedBody.ok) return NextResponse.json(parsedBody.error, { status: parsedBody.status });

    let exists: boolean;
    try {
      exists = await db.dsDeviceExists(serial);
    } catch (e) {
      const body = errorBody("internal_error", "データの取得に失敗しました");
      logApiError({ route: ROUTE, method: "POST", body, status: 500, err: e });
      return NextResponse.json(body, { status: 500 });
    }
    if (!exists) return jsonError(404, "not_found", "デバイスが見つかりません");

    const relay = getRelay();
    if (USE_FIXTURES) ensureFixtureAgent(relay, serial);

    const started = Date.now();
    const result = await relay.createExport(serial, parsedBody.query.fileIds, request.signal);
    switch (result.kind) {
      case "offline": {
        const lastSeen = relay.agentStatus(serial).last_seen_at;
        return jsonError(409, "agent_offline", "制御盤が接続されていません", {
          last_seen_at: lastSeen ? [lastSeen] : [],
        });
      }
      case "busy":
        return result.code === "export_busy"
          ? jsonError(429, "export_busy", "この制御盤は別のエクスポートを処理中です")
          : jsonError(429, "agent_busy", "制御盤が混み合っています");
      case "timeout":
        return jsonError(504, "agent_timeout", "制御盤から応答がありません");
      case "agent_error":
        return jsonError(502, "agent_error", "制御盤でエラーが発生しました", { agent_code: [result.code] });
      case "prepared": {
        logEvent("info", {
          event: "ds_logs.export_prepared",
          serial_number: serial,
          file_count: result.fileCount,
          total_bytes: result.totalBytes,
          duration_ms: Date.now() - started,
        });
        const body: DsLogExportResponse = {
          export_id: result.exportId,
          download_url: `/api/ds-devices/${encodeURIComponent(serial)}/logs/exports/${result.exportId}`,
          total_bytes: result.totalBytes,
          file_count: result.fileCount,
          expires_in_sec: result.expiresInSec,
        };
        return NextResponse.json(body, { status: 201 });
      }
    }
  });
}